// 内置的
import { existsSync, createReadStream, readFileSync } from "fs";
import fsPromises from "fs/promises";
import path from "path";
import http from "http";
import qs from "querystring"; // 内置的
import crypto from "crypto";
import url from "url";
import zlib from "zlib";

// 文件模块
import { getHosts } from "./utils.js";
import parseBody from "./parseBody.js";
import cors from "./cors.js";

// 第三方
import { v4 } from "uuid";

import ejs from "ejs";
import chalk from "chalk";
import mime from "mime";

// jsp -> smarty -> underscore -> handlers -> ejs -> jade -> nunjucks -> template
const session = {}; // -> redis / mongo
class Server {
  constructor(options) {
    this.port = options.port || 8080; // 端口
    this.address = options.address || "0.0.0.0"; // ip
    // process.cwd()  == path.resolve()
    let publicPath = path.resolve("public"); // 静态文件路径
    this.directory =
      options.directory || existsSync(publicPath) ? publicPath : process.cwd();

    // 如果用户指定了特定ip，本地可能无法使用
    this.hosts = [...new Set([this.address, ...getHosts()])];
    this.template = readFileSync(
      path.resolve(import.meta.dirname, "template.html"),
      "utf-8"
    );
    //  之前多包了一层
    this.mockFile = path.resolve("mock/index.js");
    this.secret = "jw";
  }
  compress(req, res, pathname, statObj) {
    const acceptEncoding = req.headers["accept-encoding"];
    if (acceptEncoding) {
      if (acceptEncoding.includes("gzip")) {
        res.setHeader("Content-Encoding", "gzip");
        return zlib.createGzip(); // 转化流
      } else if (acceptEncoding.includes("deflate")) {
        res.setHeader("Content-Encoding", "deflate");
        return zlib.deflate(); // 转化流
      }
    }
  }
  cache(req, res, accessPath, statObj) {
    // ----- 缓存  ----
    // 服务端和客户端说什么时候缓存过期 ：客户端会用自己的事件和服务端的比较
    //判断 浏览器访问的 资源是否没有变化，如果没有变化直接找 对比缓存 ， 强制缓存（只针对引用的资源）直接通过浏览器首次访问的资源是不能做强制缓存的（浏览器控制）
    res.setHeader("Cache-Control", "max-age=10");
    //  一般针对资源我们可以采用强制缓存（时间不要设置的很大） + 对比缓存的方式

    // last-modified: 用的是文件的最后修改时间
    // 第一次你访问我，我获取这个文件的修改时间 18.3月
    // 第二次你在访问我 需要带上 18.3月  我获取这个文件的修改时间 19.3月
    // 如果时间变化说明文件被修改了

    // let ifModifedSince = req.headers["if-modified-since"]; // .客户端访问我
    // let ctime = statObj.ctime.toLocaleString();
    // res.setHeader("Last-Modified", ctime);
    // if (ifModifedSince === ctime) {
    // 1.最后修改时间有缺陷，就是文件没有变，但是修改时间变化了
    // 2.1s 内可以修改多次，也无法监控
    // 3.不同的文件上传到cdn的时间也不一样
    // 4.最后修改时间 也是客户端发过来的，也可以篡改
    // 缓存出错 < 返回最新的  宁可返回最新的也不希望出错
    //   return true;
    // }
    // etag 指纹 md5  -》etag 摘要（浪费性能）
    // 第二次来的时候 你携带上一次的摘要   ===  和最新的摘要做对比
    // 用文件的大小 + 修改时间 （不用读取文件）

    let ifNoneMatch = req.headers["if-none-match"];
    console.log(ifNoneMatch);
    let etag =
      statObj.ctime.getTime().toString(16) + "-" + statObj.size.toString(16);

    res.setHeader("Etag", etag);

    if (ifNoneMatch === etag) {
      return true;
    }
    // etag 优先级更改
  }
  // 强制缓存 cache-control
  // 对比 etag
  processFile(req, res, accessPath, statObj) {
    let cache = this.cache(req, res, accessPath, statObj);
    if (cache) {
      res.statusCode = 304; // 走浏览器缓存  301 302 304 去缓存里找内容，服务端不返回
      return res.end();
    }
    // ----- 压缩 -----
    const compressStream = this.compress(req, res, accessPath, statObj);
    res.setHeader(
      "Content-Type",
      (mime.getType(accessPath) || "text/plain") + ";charset=utf-8"
    );
    if (compressStream) {
      // 协商是否支持压缩
      createReadStream(accessPath).pipe(compressStream).pipe(res);
    } else {
      createReadStream(accessPath).pipe(res);
    }
  }
  sendError(res, e) {
    console.log(e, "出错le");
    res.statusCode = 404;
    res.end(); // 文件不存在
  }
  processDir = async (req, res, accessPath, statObj, pathname) => {
    let dirs = await fsPromises.readdir(accessPath);
    dirs = dirs.map((item) => ({
      name: item,
      href: path.join(pathname, item),
    }));

    const template = ejs.render(this.template, { dirs });
    res.setHeader("Content-Type", "text/html" + ";charset=utf-8");
    res.end(template);
  };
  async middleware(accessPath, query, req, res) {
    // 解决跨域的多种方式 websocket(没有跨域问题)  前端代理（nginx）  跨域资源共享（服务端解决跨域）
    // 跨域的时候 会先测试（预检请求  options）能否跨域，再去发送真正的请求
    // 默认get和post 是简单请求 没有增加额外的信息（自定义header） 不会发送options
    // put  , delete  , get/post+header  都会变成复杂请求
    cors(req, res);
    req.path = accessPath;
    req.query = query;
    //  在调用用户处理逻辑前，我们可以将数据先进行简单的格式化，格式化后交给用户
    req.body = await parseBody(req);
    res.send = function (data) {
      if (Buffer.isBuffer(data)) {
        return res.end(data);
      } else if (typeof data === "object") {
        res.setHeader("Content-Type", "application/json");
        return res.end(JSON.stringify(data));
      } else if (typeof data === "string") {
        res.setHeader("Content-Type", "text/plain;charset=utf-8");
        return res.end(data);
      }
      //   ...
    };

    // cookie 本身就是一个 httpHeader
    const cookieList = [];
    res.setCookie = (key, value, options = {}) => {
      let cookie = `${key}=${value}`;
      let signedContent = cookie;
      if (options.domain) {
        cookie += `; domain=${options.domain}`;
      }
      if (options.path) {
        cookie += `; path=${options.path}`;
      }
      if (options.httpOnly) {
        cookie += `; httpOnly=${options.httpOnly}`;
      }
      if (options.maxAge) {
        cookie += `; max-age=${options.maxAge}`;
      }

      if (options.signed) {
        const signed = crypto
          .createHmac("sha1", this.secret)
          .update(signedContent)
          .digest("base64url"); // base64   + / =
        cookieList.push(`${key}.sign=${signed}`);
        // 给这个需要签名的额外增加一个cookie 用于记录签名的
      }
      cookieList.push(cookie);
      res.setHeader("Set-Cookie", cookieList);
    };
    req.getCookie = (key, options = {}) => {
      let cookie = req.headers["cookie"];
      let cookieObj = qs.parse(cookie, "; ", "=");

      if (options.signed) {
        const signed = crypto
          .createHmac("sha1", this.secret)
          .update(`${key}=${cookieObj[key]}`)
          .digest("base64url");

        if (cookieObj[`${key}.sign`] === signed) {
          return cookieObj[key];
        } else {
          return "数据被篡改";
        }
      }

      return cookieObj?.[key] || "";
    };

    let cardName = "connect.sid"; // 我开店的名字
    req.session = new Proxy(session, {
      get(target, key, receiver) {
        // 补充：如果有暂存的，说明第一次先用暂存的
        let cardId = res.cardId || req.getCookie(cardName, { signed: true });
        if (cardId && target[cardId]) {
          return target[cardId][key];
        }
      },
      set(target, key, value, receiver) {
        let cardId = req.getCookie(cardName, { signed: true });
        if (cardId && target[cardId]) {
          target[cardId] = { [key]: value };
        } else {
          const cardId = v4();
          res.setCookie(cardName, cardId, { signed: true });
          target[cardId] = { [key]: value };
          // 补充：这里设置了cookie后，浏览器没有立刻渲染，需要暂存
          res.cardId = cardId;
        }
        return true;
      },
    });
  }
  async dynamicServer(pathname, query, req, res) {
    let returnVal = false;
    if (pathname.startsWith("/api")) {
      let accessPath = pathname.replace("/api", "");
      // accessPath -> /user
      let exists = existsSync(this.mockFile);
      if (exists) {
        // file://c://xxxx/xxxx/xxxx -> windows 系统下 会认为盘符是协议
        // ESM import('文件路径')
        let { default: mockFn } = await import(
          url.pathToFileURL(this.mockFile)
        );
        // 中间件 （增加公共的属性和方法）
        await this.middleware(accessPath, query, req, res);
        returnVal = await mockFn(req, res);
      }
    }
    return returnVal;
  }

  handleRequest = async (req, res) => {
    // this 指向的依旧是 myServer的实例
    // 服务端需要根据请求的路径来处理 1.资源路径(xx.html)  2.动态路径（请求）
    try {
      let { pathname, query } = url.parse(req.url, true);
      // 处理乱码问题
      pathname = decodeURIComponent(pathname);

      //   在这里去处理 /api开头的路径 说明是动态的，交给用户自己写的接口来处理
      let isDynammic = await this.dynamicServer(pathname, query, req, res);
      if (isDynammic) {
        return;
      }

      const accessPath = path.join(this.directory, pathname);
      const statObj = await fsPromises.stat(accessPath);
      if (statObj.isFile()) {
        // 通过流的方式将内容返回即可
        this.processFile(req, res, accessPath, statObj);
      } else {
        this.processDir(req, res, accessPath, statObj, pathname);
      }
    } catch (e) {
      this.sendError(res, e);
    }
  };
  start() {
    const server = http.createServer(this.handleRequest);
    server.listen(this.port, this.address, () => {
      let startDirectory = "./" + path.relative(process.cwd(), this.directory);
      console.log(
        `${chalk.yellow(
          `Starting up http-server, serving ${chalk.blueBright(startDirectory)}`
        )}`
      );
      console.log(chalk.yellow(`Available on:`));
      this.hosts.forEach((host) => {
        console.log(` http://${host}:${chalk.green(this.port)}`);
      });
      console.log("Hit CTRL-C to stop the server");
    });
  }
}

export default Server;

// session 如果存储的是登录状态 connect.sid -》 卡号
// 我们希望 共享登录状态
// -> h5  -> connect.sid -》 卡号

// session 的登录信息我们存到数据库中

// jwt  (jsonwebtoken 标识令牌的过期时间，令牌里存放的特殊信息)
